feat: restrict stablecoin currency to ISO 4217 fiat allowlist#46
Merged
Conversation
Adds `ISO4217.sol` library exposing two primitives the `TokenFactory` mock now uses to validate `B20StablecoinCreateParams.currency`: an `isValidFiatCode` allowlist of circulating national fiat codes, and an enumerable `excludedAt` / `excludedCount` blocklist of ISO 4217 entries that are deliberately rejected (precious metals, supranational synthetics, sentinels, funds codes) with per-entry rationale. Replaces the prior empty-string-only check on `currency` with the allowlist enforcement; rejections surface through a new `InvalidCurrency(string)` error carrying the offending input. Updates the stablecoin and factory natspec to describe the trust model (self-declared identifier, not a trust signal — consumers bring their own admission logic) and the scope (X-prefix is not categorical; multi-country fiat codes XOF/XAF/XCD/XPF are included). Consolidates the currency-validation test surface from 15 point tests to 5: two explicit successes (majors + multi-country X-prefix), two fuzz reverts (universal non-allowlist rejection, blocklist enumeration), and one atomicity test. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Documents why this variant is restricted to single-fiat tracking even though "stablecoin" is used in industry and regulation to describe a wider set of instruments. Anchors the narrowing to MiCA E-Money Tokens, MAS Single-Currency Stablecoins, and US payment-stablecoin legislative definitions — the regulatory regimes that have already sub-divided the term along the same line we draw. Calls out specifically that commodity-backed tokens marketed as stablecoins (PAXG, XAUT) belong on IB20Security; that crypto- collateralized fiat-pegged tokens (DAI, LUSD) fit this variant since the peg target — not the collateral mechanism — is what currency() expresses; and that basket-pegged and algorithmic non-pegged tokens have no current B-20 home. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Moves the scope, exclusion-category reasoning, regulatory framing (MiCA EMT / MAS SCS / FSB / BIS), and trust model out of the IB20Stablecoin, ITokenFactory, and ISO4217 natspec into a single canonical docs/iso4217-filter.md. The new file presents inclusion / exclusion as a scannable table and totals ~26 lines. Natspec now stays at the API-contract layer: what each function does, what reverts, with one-line pointers to docs/ for the deeper context. Per-entry rationale in ISO4217.excludedAt stays inline since it answers "why isn't X on the list?" at the exact location a reader would ask. Net: -217 natspec lines, +26 docs lines. IB20Stablecoin.sol is now 20 lines total. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…and to full problem/solution/risks Restructures the docs file into a four-section flow that's self-describing on its own: problem statement (why constrain currency at all), solution (ISO 4217 fiat subset), complete specification (the inclusion/exclusion table), and risks + mitigations (PAXG-class commodities go to Security; DAI-class crypto-collateralized still fit; basket/algorithmic have no home; naming friction with industry terminology is anchored in MiCA / MAS precedent; trust model is consumer-layered). Moves the file from docs/iso4217-filter.md to docs/b20/stablecoin/currency-validation.md so the docs taxonomy mirrors the interface hierarchy (B20 / B20Stablecoin / concept). Updates all natspec links to the new path and trims two more small natspec sprawls — the X-prefix inline comment and the isValidFiatCode @notice — that duplicated content now in docs. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
MockTokenFactory's 7-line ISO-4217 explainer collapses to one line pointing at docs/. The five currency tests in createToken.t.sol each drop to a 2-line @notice + @dev docblock; the section header collapses from a 12-line preamble to a single pointer comment. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Tests with arguments are fuzzed by default in Foundry; the suite
convention is test_{function}_{condition}_{case} regardless of
whether the body uses fuzz inputs.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replaces the per-comparison `keccak256("AED")` form with a block of
named `bytes3 private constant` declarations at the top of the
library, and a comparison chain that reads `c == AED || c == AFN ||
...`. The canonical 155-entry list is now visible as data, easier
to scan against ISO 4217, and easier to audit for additions and
removals.
Honest tradeoff: per-call gas is ~2× higher than the keccak chain
(the Solidity optimizer was already constant-folding the keccak
literals, so the old version was paying ~1 keccak on the input
plus 155 PUSH32 EQ; the new version pays no keccak but ~155 PUSH3
EQ with additional control-flow overhead). Impact is mock-test
runtime only; the Rust precompile will hash to O(1) regardless.
A first-byte-dispatch optimization (`if (first == "U") return c ==
USD || ...`) would bring gas below the keccak version while keeping
the readability win; deferred until reviewer signs off on the
readability direction.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Wraps the bytes3 comparison chain in an outer `if (first == "X")` dispatch on the input's first byte, so each call evaluates at most one letter bucket instead of all 26. Worst-case ≈ 16 word-equality comparisons (S bucket, 15 entries) plus a few first-byte branches, vs ≈ 155 for a flat chain. Per-call gas (micro-bench, 100 calls each): - keccak (original): 4.2k / call - bytes3 flat: 7.9k / call - this commit: 1.4k / call (~3× faster than keccak) Adds an @dev natspec on `isValidFiatCode` documenting the O(1) amortized characteristic and the worst-case bucket size. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sorts entries within each first-byte bucket so the highest-volume codes short-circuit first. USD/EUR/JPY/GBP/CHF/CAD/AUD/SEK/NOK/NZD (G10) lead their respective buckets, with CNY, INR, MXN, BRL, etc. following close behind. Also reorders the outer letter-dispatch so the buckets containing the G10 and top-FX currencies are checked first (U/E/J/G/C/A/N/S/I/M/T/P/K/B/H/R/D/X/Z/V/L), keeping single-entry buckets at the end. Per-call gas (micro-bench, 100 calls each): - USD: ~370 gas (was 1.4k; ~11× the original keccak chain's 4.2k) - EUR: ~400 gas - JPY: ~430 gas - ZWG (late): ~1.3k - ZZZ (miss): ~1.3k Mean for real-world stablecoin traffic — overwhelmingly USD with EUR a distant second — is effectively two comparisons (first-byte + first match), per the power-law observation that drove the sort. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The library is consumed only by the mock factory and test fuzzers, so it doesn't belong under src/ — moving to test/lib/ alongside the other mock-supporting code keeps the src/ surface focused on the interfaces consumers actually import. The src/utils/ directory is removed (it had no other entries). Also appends a "Supported currencies" table to the docs page listing all 155 allowlist codes alphabetically with currency name and region/issuer. Useful as a self-contained reference so readers don't have to map ISO 4217 codes to currencies themselves. Updates the docs-file path link from src/utils/ to test/lib/ and the natspec import paths in MockTokenFactory and createToken.t.sol. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Captures the five validation approaches surfaced during design (no validation, format-only, full ISO 4217, narrow ISO 4217 fiat, off- chain registry) with a one-line rationale for why each non-chosen option was set aside. Marks the chosen approach inline so the decision is visible without forcing readers to cross-reference the solution section. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Bolds the option title, moves the description underneath with a `<br>` separator, and splits the per-option rationale into bulleted Pros and Cons columns. Easier to scan; trade-offs land on both sides of the line rather than only as a single "why not" sentence. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… row Adds manual `<br>` breaks within each Option-column description so the column stops sprawling and the Pros/Cons columns get balanced width. Removes the off-chain registry row — it's a non-option (no on-chain validation defeats the whole purpose). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/utils/ISO4217.sollibrary exposing two primitives theTokenFactorymock uses to validateB20StablecoinCreateParams.currency:isValidFiatCode(string)— allowlist of active ISO 4217 circulating-fiat alphabetic codesexcludedAt(uint256)/excludedCount()— enumerable blocklist of ISO 4217 entries deliberately rejected (precious metals, supranational synthetics, sentinels, funds codes), each with inline rationalecurrencywith full allowlist enforcement. Rejections surface through a newInvalidCurrency(string code)error that carries the offending input verbatim for diagnostics.IB20StablecoinandITokenFactory.B20StablecoinCreateParams.currencynatspec to document scope, trust model, regulatory alignment, and the friction this variant accepts by narrowing the term "stablecoin."Scope: what's in, what's out
Included (any of these will round-trip through
currency()):Explicitly excluded (enumerated in
ISO4217.excludedAtwith per-entry rationale, fuzz-tested bytest_fuzz_createToken_revert_currency_blocklist):Risks of the narrow scope, and mitigations
The headline risk is that "stablecoin" is used in industry and regulation more broadly than this variant accepts. FSB and BIS define stablecoins as any crypto-asset tracking any asset or basket. This variant scopes to single-fiat tracking specifically.
IB20Securityvariant, notIB20Stablecoin. Documented inIB20Stablecoinnatspec.currency(); what matters is the peg target. If a token pegs to USD, declare"USD"regardless of whether the backing is custodial reserves, on-chain collateral, or T-bills. Documented in natspec.B20Default variant with custom monetary policy would be the path, not relaxation of this variant.currency()for authorization MUST layer its own issuer/contract allowlist on top.Reviewer asks
IB20Stablecoinnatspec — particularly the "commodities go on Security" position — matches the broader variant-roadmap intentISO4217.solexactly; flag this for whoever picks up the precompile workTest plan
forge build— cleanforge test— 350 tests pass (down from 359 pre-consolidation; net -9 from collapsing 15 currency tests into 5, plus 1 new multi-country X-prefix success test)vm.assume(!isValidFiatCode(input)), asserting revert + diagnostic round-tripseed % excludedCount()across all 22 documented blocklist entries🤖 Generated with Claude Code